【UE4学习】20200826 C++11 移动语义、拷贝构造函数、赋值运算符 |
您所在的位置:网站首页 › 右值引用 构造函数 › 【UE4学习】20200826 C++11 移动语义、拷贝构造函数、赋值运算符 |
右值引用与TArray、TString参考文献
虚幻4与现代C++: 转移语义和右值引用
一次性搞定右值,右值引用(&&),和move语义
谈谈 C++ 中的右值引用
引子 前文讲到一个点: 12345// 差 - 返回常量数组const TArray GetSomeArray();// 优 - 返回常量数组的引用const TArray& GetSomeArray();那为啥要这么写呢?这其中涉及到了右值引用这个知识点。 右值引用要解决啥问题我们知道,C++中把值对象(包括栈上面分配的任何内存:基元类型或任何不是new出来的类实例)作为右值赋值给另外一个左值的时候,会触发复制。 比如: 12345678910111213141516171819202122class Test{public: int32 Value; /** 拷贝构造函数 */ Test(const Test& Another) { Value = Another.Value; }}Test Foo(){ Test Ret = Test(); return Ret;}int main(){ Test t = Foo();}在上面这段代码中,要注意两点: 实际上Foo()真正返回到main函数中的对象和Foo中的Ret不是同一个,而是经过值复制得来的。 拿到Foo()的返回值之后,还需要执行拷贝构造函数,才能得到对象这就造成了一个没有必要的消耗,也就是多了一次值复制的过程。 按照知乎帖子的神比喻来说: 如何将大象从一台冰箱转移到另一台冰箱?普通解答:打开冰箱门,取出大象,关上冰箱门,打开另一台冰箱门,放进大象,关上冰箱门。 2B解答:在第二个冰箱中启动量子复制系统,克隆一只完全相同的大象,然后启动高能激光将第一个冰箱内的大象气化消失。 等等,这个2B解答听起来很耳熟,这不就是C++中要移动一个对象时所做的事情吗? 其实这样子设计也可以理解,毕竟Ret是分配于栈上的一个临时变量,当超出Foo()的定义域的时候就应该消失,此时外界还想要获取到该值,就应该通过值复制拷贝一份出去。 所以我们要解决这个不必要的值复制的问题。 怎么解决呢?临时变量不要被回收就好了。 既然临时变量要被回收,那么就意味着它不能再被其他任何地方用到。那么,在法律上属于被遗弃品,我们拾取了它,转移它的所有权即可。 所谓的右值引用,实质上就是一个「转移所有权」的过程,它把一个右值的所有权赋予了一个左值。(相对的,左值引用的意义比较简单,就是相当于给一个左值起了别名。) 左值右值傻傻分不清看了那么多博客,这里直接说个结论吧。 所谓左值右值,乍看一下就是等号的左右边的值。其实不然,左右值的本质区别在于: 对于左值,指的是变量的存储位置 对于右值,指的是变量的值;可以是一个常量,也可以是函数返回的变量 右值引用前面简单地提到了右值引用。所谓右值引用,可以简单地理解为「控制权转移」。 有了右值引用,C++世界变得有效率得多了。当使用右值赋值给左值的时候,可以直接将右值的控制权移交给左值,而不是先把右值复制一遍,再交给左值。 在右值引用之前,如果将参数传进来,一般用的是const左值引用,如下: 123456789void Foo(const string& str);void main(){ string a = "a"; Foo(a); // 没有复制,没有性能损耗 Foo("rvalue"); // 会产生一次转换}如果我们编写了右值引用,那就会直接采用右值引用的版本 1void Foo(string&& str);为了避免每次都要定义两个版本的函数,我们可以只定义一个右值引用的版本,然后采用std::move()把左值引用都变成右值引用。 右值引用代表着原拥有者放弃了所有权,由于一般都是临时变量,所以放弃了所有权也就放弃了,随便别人拿到所有权后咋整都行。 再举个例子。在右值引用和左值引用函数同时存在(重写了)时,如果传进来的是右值,会优先使用右值引用版本的函数。 1234567891011121314151617181920212223242526272829class Test{public: // 可能是一个大数组 int* Data; Test() : Data(new int[10000000]) {} Test(const Test& Another) { /**肯定不能够将Another的Data的地址赋值给自己,而是要进行深度复制, * 把整个数组复制一遍,才能够防止数据污染 */ std::copy(Another.Data, Another.Data + 10000000, this->Data); }}void Func(Test t){ // ...}void main(){ Person p; func(p); // Func的参数是值传递,会隐式调用`Test(const Test& Another)`来构建参数}上面这个例子其实也没啥问题,传进去一个左值p,这个左值我们可能在main函数的定义域内还要做其他修改,当然不能够简单地浅复制解决。 但是假如我们是传进一个临时变量,那么再把这个大数组进行完整地复制,那就是多此一举了。最好的就是浅复制,直接拿到这个int数组。 1234void main(){ func(Person());}这个时候我们要多提供一个右值引用的构造函数: 123456789/** * 进来这个版本的构造函数了,说明传进来的是右值,可以毫不留情地掠夺资源*/Test(Test&& Another){ this->Data = Another.Data; /** 把右值对象的内容置空,避免源对象析构时影响本对象 */ Another.Data = nullptr;}如果我们能够保证左值在传入右值引用函数之后,不再使用,那么我们也可以把左值赋予给右值引用(抛弃自己的所有权,但是只能我们自己保证,编译器不会禁止继续使用和更改该左值)。 12345void main(){ Person p; func(move(p));} 禁止默认的const Class&构造函数1Test(const Test& Another) = delete;如果我们delete了默认的拷贝构造函数,只实现移动构造函数,那么在使用拷贝构造函数的时候(不管隐式还是显示)就会编译报错,相当于强迫使用者使用右值引用。 1234Person a;Func(a); // error 左值不能转换成右值引用Func(std::move(a)); // ok a被转换成右值,但是最好保证后面不会在a的定义域再使用到aFunc(Person()); // ok 最后再举个例子12345678910void Func(){ Test ret; return ret;}int main(){ Test a = Func();}上面这个过程总共发生了三次构造: Func()中使用默认构造函数构建了左值ret Func()返回了一个值,众所周知函数的传入与传出都是要进行一次值复制的,所以进行一次复制 Test a使用Func()返回的结果作为参数传给构造函数假设Test类中含有很大的一个数组,光是使用Test(const Test& Another)这种拷贝构造函数,我们没有办法区分清楚传进来的是左值还是右值,所以只能一律当做左值来处理。 如果是左值,那意味着传进来的这个引用,外界还会用到,并且有可能会对其内容做修改。所以我们需要进行深复制,把整个大数组拷贝一份。而如果传进来的是右值,那么亏了,明明我们知道这个右值被传进来使用过一次之后就会被外界销毁(右值都是一些会被销毁的临时变量),但是因为我们不知道究竟是不是右值,所以还是要把大数组整个拷贝一次。 而如果我们重写了一个Test(Test&& Another),当传进来的是一个右值的时候,会优先调用此构造函数。此时我们明确知道传进来的是一个右值,阅后即焚的那种,所以我们不必担心我们的修改会影响到这个变量。因此,我们直接把这个右值的内容拿过来用就行了。也就是说,直接把数组的地址拷贝过来,而不需要拷贝一整个数组。 拷贝构造函数&赋值运算符的区别本质区别:拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。 换言之,一个会生成新的实例,而一个是对已存在实例进行数据更新。 1234567891011121314151617181920void Func(Person p){}void Func2(){ Person p; return p;}int main(){ Person p; Person p1 = p; // 构造 Person p2; p2 = p; // 复制 Func(p); // 构造 p2 = Func2(); // 先构造(`Func2`返回一个值对象),然后复制给p2 Person p3 = Func2(); // 本来是应该用构造创建一个函数返回值,然后再调用构造生成p3. //但是编译器优化过,导致直接用返回的右值赋予了给p3。} |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |